/**
 *  @file SymmetricCipher.cpp
 */
#include "openssl/ssl.h"
#include "openssl/evp.h"
#include "SymmetricCipher.h"
#include "../__internal/InitOpenSSL.h"
#include "../../exceptions/ArgumentException.h"
#include "../../exceptions/OutOfBoundsException.h"
#include "../../common/UtilFuncT.h"

using namespace cppflib::exceptions;
using namespace cppflib::collections;

namespace cppflib
{

namespace security
{

namespace crypto
{

PRIVATE struct _cipheralgo_st {
           SymmetricCipher::cipheralgo_t algo;
           SymmetricCipher::blockmode_t blockMode;
           const char * algoName;
        } cipherAlgoTbl[] = {
                { SymmetricCipher::CA_AES128, SymmetricCipher::BM_CBC, "aes-128-cbc"},
                { SymmetricCipher::CA_AES192, SymmetricCipher::BM_CBC, "aes-192-cbc"},
                { SymmetricCipher::CA_AES256, SymmetricCipher::BM_CBC, "aes-256-cbc"},
                
                { SymmetricCipher::CA_DES, SymmetricCipher::BM_CBC, "des-cbc"},
                
                { SymmetricCipher::CA_3DES3, SymmetricCipher::BM_CBC, "des-ede3-cbc"},
                                
                { SymmetricCipher::CA_IDEA, SymmetricCipher::BM_CBC, "idea-cbc"},
                
                { SymmetricCipher::CA_RC2, SymmetricCipher::BM_CBC, "rc2-cbc"},
                
                { SymmetricCipher::CA_CAMELLIA128, SymmetricCipher::BM_CBC, "camellia-128-cbc"},
                { SymmetricCipher::CA_CAMELLIA256, SymmetricCipher::BM_CBC, "camellia-256-cbc"},
                
                { SymmetricCipher::CA_SEED, SymmetricCipher::BM_CBC, "seed-cbc"},
                { SymmetricCipher::CA_NULL, SymmetricCipher::BM_NONE, NULL}
                            };

PRIVATE const char * __GetNameByCipherAlgo(SymmetricCipher::cipheralgo_t algo, 
                                           SymmetricCipher::blockmode_t bm)
{
   struct _cipheralgo_st *pCipherAlgoTbl = cipherAlgoTbl;
   while (pCipherAlgoTbl->algoName) {
      if (pCipherAlgoTbl->algo == algo && pCipherAlgoTbl->blockMode == bm)
         return pCipherAlgoTbl->algoName;
      ++pCipherAlgoTbl;
   }

   return NULL;
}

PRIVATE EVP_CIPHER_CTX * __GetCipherCtxt(ptrany_t ctxt)
{
   EVP_CIPHER_CTX *pCtxt = static_cast<EVP_CIPHER_CTX*>(ctxt);
   return pCtxt;
}

PRIVATE void __CheckCipherCtxtInitialized(ptrany_t ctxt)
{
   if (!__GetCipherCtxt(ctxt)->cipher)
      throw ArgumentException(_S("Call 'Reinitialize' to init the context before use"));
}

SymmetricCipher::SymmetricCipher(void)
{
   cppflib::security::__internal::InitOpenSSL();
   this->cipherCtxt = NULL;
}

/**
 *  @param [in] isEncrypt -- true if this instance is to encrypt data;
 *                           false to decrypt data
 */
SymmetricCipher::SymmetricCipher(SymmetricCipher::cipheralgo_t cipherAlgo, 
                                 SymmetricCipher::blockmode_t blockMode, bool isEncrypt)
{
   cppflib::security::__internal::InitOpenSSL();

   this->cipherAlgo = cipherAlgo;
   this->blockMode = blockMode;
   this->isEncrypt = isEncrypt;
   this->cipherCtxt = NULL;
   
   const char * pCipherAlgoName = __GetNameByCipherAlgo(cipherAlgo, blockMode);
   if (!pCipherAlgoName)
      THROW_CTOR_EXCEP(ArgumentException, _S("Cipher algorithm not found"));

   // init message digest context
   this->cipherCtxt = ::MRP_Calloc(1, sizeof(EVP_CIPHER_CTX));
   if (!Reinitialize())
      THROW_CTOR_EXCEP(ArgumentException, _S("Cipher algorithm doesn't exist"));
}

SymmetricCipher::~SymmetricCipher(void)
{
   ReleaseBeforeThrowing();
}

void SymmetricCipher::ReleaseBeforeThrowing()
{
   if (cipherCtxt) {
      EVP_CIPHER_CTX_cleanup(__GetCipherCtxt(cipherCtxt));
      cppmempool::Release(cipherCtxt);
      cipherCtxt = NULL;
   }

   cppflib::security::__internal::UninitOpenSSL();
}

/**
 *  Call this to reinitialize the cipher context ready for next encryption or decryption
 */
bool SymmetricCipher::Reinitialize()
{
   EVP_CIPHER_CTX_cleanup(__GetCipherCtxt(cipherCtxt)); // in case it is not cleaned yet
   const EVP_CIPHER * cipher = EVP_get_cipherbyname(__GetNameByCipherAlgo(cipherAlgo, blockMode));
   if (!cipher) 
      return false;

   if (isEncrypt)
      EVP_EncryptInit(__GetCipherCtxt(cipherCtxt), cipher, NULL, NULL);
   else
      EVP_DecryptInit(__GetCipherCtxt(cipherCtxt), cipher, NULL, NULL);

   return true;
}

CFString SymmetricCipher::GetCipherName()
{
   __CheckCipherCtxtInitialized(cipherCtxt);
   return CFString(EVP_CIPHER_name(EVP_CIPHER_CTX_cipher(__GetCipherCtxt(cipherCtxt))));
}

SymmetricCipher::cipheralgo_t SymmetricCipher::GetCipherAlgorithm()
{
   return this->cipherAlgo;
}

SymmetricCipher::blockmode_t SymmetricCipher::GetBlockMode()
{
   return this->blockMode;
}

bool SymmetricCipher::IsForEncryption()
{
   return this->isEncrypt;
}

/**
 *  @param[in] lenInByte -- length in byte
 */
void SymmetricCipher::SetKeyLength(u32_t lenInByte)  
{
   __CheckCipherCtxtInitialized(cipherCtxt);
   EVP_CIPHER_CTX_set_key_length(__GetCipherCtxt(cipherCtxt), static_cast<int>(lenInByte));
}

/**
 *  Set encryption/decryption key (for ECB mode only)
 */
void SymmetricCipher::SetKey(collections::ArrayByte & key)
{
   __CheckCipherCtxtInitialized(cipherCtxt);
   
   if (key.GetSize() != GetKeyLength())
      throw ArgumentException(_S("key size incorrect. Call 'GetKeyLength' to see."));

   if (isEncrypt)
      EVP_EncryptInit(__GetCipherCtxt(cipherCtxt), NULL, key.GetRawPtr(), NULL);
   else
      EVP_DecryptInit(__GetCipherCtxt(cipherCtxt), NULL, key.GetRawPtr(), NULL);
}

/**
 *  Set encryption/decryption key and initialization vector (for non-ECB mode)
 */
void SymmetricCipher::SetKeyAndIV(ArrayByte & key, ArrayByte & iv)
{
   __CheckCipherCtxtInitialized(cipherCtxt);

   if (key.GetSize() != GetKeyLength())
      throw ArgumentException(_S("key size incorrect. Call 'GetKeyLength' to see."));

   if (iv.GetSize() != GetIVLength())
      throw ArgumentException(_S("IV size incorrect. Call 'GetIVLength' to see."));

   if (isEncrypt)
      EVP_EncryptInit(__GetCipherCtxt(cipherCtxt), NULL, key.GetRawPtr(), iv.GetRawPtr());
   else
      EVP_DecryptInit(__GetCipherCtxt(cipherCtxt), NULL, key.GetRawPtr(), iv.GetRawPtr());
}

/**
 * Enable/disable padding (PKCS7)
 */
void SymmetricCipher::EnablePadding(bool enabled)
{
   __CheckCipherCtxtInitialized(cipherCtxt);
   EVP_CIPHER_CTX_set_padding(__GetCipherCtxt(cipherCtxt), enabled ? 1 : 0);
}

/**
 * Get key length in byte 
 */
u32_t SymmetricCipher::GetKeyLength()
{
   __CheckCipherCtxtInitialized(cipherCtxt);
   return EVP_CIPHER_CTX_key_length(__GetCipherCtxt(cipherCtxt));
}

/**
 * Get block size in byte
 */
u32_t SymmetricCipher::GetBlockSize()
{
   __CheckCipherCtxtInitialized(cipherCtxt);
   return EVP_CIPHER_CTX_block_size(__GetCipherCtxt(cipherCtxt));
}

/**
 * Get IV length in byte
 */
u32_t SymmetricCipher::GetIVLength()
{
   __CheckCipherCtxtInitialized(cipherCtxt);
   return EVP_CIPHER_CTX_iv_length(__GetCipherCtxt(cipherCtxt));
}

PRIVATE void __CheckInOutDataBlock(ArrayByte *pOutData, u32_t outOffset, bool chkOutBlkSize,
                                   ArrayByte *pInData, u32_t inOffset, bool chkInBlkSize, 
                                   u32_t blockSize)
{
   if (pOutData && outOffset >= pOutData->GetSize())
      throw OutOfBoundsException(_S("ouput offset out of bound"));

   if (pInData) {
      if (!chkInBlkSize) { // for encrypt final, inOffset can be the same as size of pInData
         if (inOffset > pInData->GetSize())
            throw OutOfBoundsException(_S("input offset out of bound"));
      }
      else {
         if (inOffset >= pInData->GetSize())
            throw OutOfBoundsException(_S("input offset out of bound"));
      }
   }

   if (pOutData && chkOutBlkSize) {
      if ((pOutData->GetSize() - outOffset) < blockSize)
         throw RuntimeException(_S("Size of output buffer should be at least one block"));
   }

   if (pInData && chkInBlkSize) {
      if ((pInData->GetSize() - inOffset) < blockSize)
         throw RuntimeException(_S("Size of input data should be at least one block"));
   }


}

/**
 *  Encrypt a block of data (GetBlockSize) once at a time
 *
 *  @note 'inData' must have at least one block of data after applying 'inOffset'
 *  @note 'out' must have at least one block of space after applying 'outOffset'
 *
 *  @param [out] offsetUpdate -- contains number of bytes encrypted in last block
 */
bool SymmetricCipher::EncryptUpdate(ArrayByte &out, u32_t outOffset, 
                                    ArrayByte &inData, u32_t inOffset, u32_t &offsetUpdate)
{
   if (!isEncrypt)
      throw RuntimeException(_S("This context is for encryption"));

   int blockSize = GetBlockSize();
   __CheckCipherCtxtInitialized(cipherCtxt);
   __CheckInOutDataBlock(&out, outOffset, true, &inData, inOffset, true, blockSize);
   
   return (EVP_EncryptUpdate(__GetCipherCtxt(cipherCtxt),
                             out.GetRawPtr() + outOffset, reinterpret_cast<int*>(&offsetUpdate),
                             inData.GetRawPtr() + inOffset, blockSize) != 0);
}

/**
 *  To encrypt the last block of data
 *
 *  @param [in] inLen -- length of last block 
 *                       pass 0 if input data size has already been consumed by EncryptUpdate
 *                       above (i.e. data size is a multiple of block size)
 *
 *  @param [out] offsetUpdate -- contains number of bytes encrypted in last block
 */
bool SymmetricCipher::EncryptFinal(ArrayByte &out, u32_t outOffset, 
                                   ArrayByte &inData, u32_t inOffset, u32_t inLen,
                                   u32_t &offsetUpdate)
{
   if (!isEncrypt)
      throw RuntimeException(_S("This context is for encryption"));

   int blockSize = GetBlockSize();
   __CheckCipherCtxtInitialized(cipherCtxt);
   // inData can be less than one block here
   __CheckInOutDataBlock(&out, outOffset, true, &inData, inOffset, false, blockSize);
   
   u32_t tempOutOffset = outOffset;
   offsetUpdate = 0;
   inLen = cppflib::Min<u32_t>(inData.GetSize() - inOffset, inLen);
   if (inLen > 0) {
      if (EVP_EncryptUpdate(__GetCipherCtxt(cipherCtxt),
                            out.GetRawPtr() + outOffset, reinterpret_cast<int*>(&offsetUpdate),
                            inData.GetRawPtr() + inOffset, inLen) == 0)
         return false;

      tempOutOffset += offsetUpdate;
   }

   u32_t finalBlockUpdate = 0;
   bool ret = (EVP_EncryptFinal(__GetCipherCtxt(cipherCtxt),
                                out.GetRawPtr() + tempOutOffset, 
                                reinterpret_cast<int*>(&finalBlockUpdate)) != 0);
   offsetUpdate += finalBlockUpdate;
   return ret;
}

/**
 *  Decrypt a block of data (GetBlockSize) once at a time (not the last block)
 *
 *  @note 'inData' must have at least one block of data after applying 'inOffset'
 *  @note 'out' must have at least one block of space after applying 'outOffset'
 *
 *  @param [out] offsetUpdate -- contains number of bytes decrypted in last block
 */
bool SymmetricCipher::DecryptUpdate(ArrayByte &out, u32_t outOffset, 
                                    ArrayByte &inData, u32_t inOffset, u32_t &offsetUpdate)
{
   if (isEncrypt)
      throw RuntimeException(_S("This context is for decryption"));

   int blockSize = GetBlockSize();
   __CheckCipherCtxtInitialized(cipherCtxt);
   __CheckInOutDataBlock(&out, outOffset, true, &inData, inOffset, true, blockSize);

   return (EVP_DecryptUpdate(__GetCipherCtxt(cipherCtxt),
                             out.GetRawPtr() + outOffset, reinterpret_cast<int*>(&offsetUpdate),
                             inData.GetRawPtr() + inOffset, blockSize) != 0);
}

/**
 *  Decrypt the last block of data
 *
 *  @param [out] offsetUpdate -- contains number of bytes decrypted in last block
 *
 *  @return false if padding enabled but not correct
 */
bool SymmetricCipher::DecryptFinal(ArrayByte &out, u32_t outOffset, u32_t &offsetUpdate)
{
   if (isEncrypt)
      throw RuntimeException(_S("This context is for decryption"));

   int blockSize = GetBlockSize();
   __CheckCipherCtxtInitialized(cipherCtxt);
   __CheckInOutDataBlock(&out, outOffset, true, NULL, 0, false, blockSize);

   return (EVP_DecryptFinal(__GetCipherCtxt(cipherCtxt),
                            out.GetRawPtr() + outOffset, 
                            reinterpret_cast<int*>(&offsetUpdate)) != 0);
}


} // end of namespace crypto

} // end of namespace security

} // end of namespace cppflib
